/*
 * Unidata Platform Community Edition
 * Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 * This file is part of the Unidata Platform Community Edition software.
 *
 * Unidata Platform Community Edition is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Unidata Platform Community Edition is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */

package org.unidata.mdm.meta.service.impl.data;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.unidata.mdm.core.service.MetaModelService;
import org.unidata.mdm.core.type.model.AttributeElement;
import org.unidata.mdm.core.type.model.AttributeElement.AttributeValueType;
import org.unidata.mdm.core.type.model.CustomPropertyElement;
import org.unidata.mdm.core.type.model.EntityElement;
import org.unidata.mdm.core.type.model.ModelSearchEntry;
import org.unidata.mdm.core.type.model.RelationElement;
import org.unidata.mdm.core.type.model.StorageElement;
import org.unidata.mdm.core.util.SecurityUtils;
import org.unidata.mdm.meta.configuration.Descriptors;
import org.unidata.mdm.meta.configuration.MetaConfigurationConstants;
import org.unidata.mdm.meta.exception.MetaExceptionIds;
import org.unidata.mdm.meta.service.MetaCustomPropertiesConstants;
import org.unidata.mdm.meta.type.instance.DataModelInstance;
import org.unidata.mdm.meta.type.search.EntityIndexType;
import org.unidata.mdm.meta.type.search.EtalonIndexType;
import org.unidata.mdm.meta.type.search.ModelHeaderField;
import org.unidata.mdm.meta.type.search.ModelIndexType;
import org.unidata.mdm.meta.type.search.RecordHeaderField;
import org.unidata.mdm.meta.type.search.RelationHeaderField;
import org.unidata.mdm.meta.util.ModelUtils;
import org.unidata.mdm.search.context.IndexRequestContext;
import org.unidata.mdm.search.context.MappingRequestContext;
import org.unidata.mdm.search.context.SearchRequestContext;
import org.unidata.mdm.search.service.SearchService;
import org.unidata.mdm.search.type.form.FieldsGroup;
import org.unidata.mdm.search.type.form.FormField;
import org.unidata.mdm.search.type.indexing.Indexing;
import org.unidata.mdm.search.type.indexing.IndexingField;
import org.unidata.mdm.search.type.indexing.impl.IndexingRecordImpl;
import org.unidata.mdm.search.type.mapping.Mapping;
import org.unidata.mdm.search.type.mapping.MappingField;
import org.unidata.mdm.search.type.mapping.impl.BooleanMappingField;
import org.unidata.mdm.search.type.mapping.impl.CompositeMappingField;
import org.unidata.mdm.search.type.mapping.impl.DateMappingField;
import org.unidata.mdm.search.type.mapping.impl.DoubleMappingField;
import org.unidata.mdm.search.type.mapping.impl.LongMappingField;
import org.unidata.mdm.search.type.mapping.impl.StringMappingField;
import org.unidata.mdm.search.type.mapping.impl.TimeMappingField;
import org.unidata.mdm.search.type.mapping.impl.TimestampMappingField;
import org.unidata.mdm.search.type.query.SearchQuery;
import org.unidata.mdm.search.util.SearchUtils;
import org.unidata.mdm.system.exception.PlatformFailureException;
import org.unidata.mdm.system.service.AfterModuleStartup;
import org.unidata.mdm.system.service.AfterPlatformStartup;
import org.unidata.mdm.system.service.ConfigurationActionService;
import org.unidata.mdm.system.type.action.ConfigurationAction;

/**
 * @author Mikhail Mikhailov on Oct 14, 2019
 */
@Component
public class DataModelMappingComponent implements AfterPlatformStartup, AfterModuleStartup {
    /**
     * The logger.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(DataModelMappingComponent.class);
    /**
     * Config action.
     */
    private final ConfigurationAction ensureIndexesAction = new ConfigurationAction() {
        /**
         * {@inheritDoc}
         */
        @Override
        public int getTimes() {
            return 1;
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public String getId() {
            return MetaConfigurationConstants.META_ENSURE_INDEXES_ACTION_NAME;
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public boolean execute() {

            try {
                ensureModelIndexes();
                ensureDataIndexes();
            } catch (Exception e) {
                LOGGER.warn("Cannot ensure search indexes. Exception caught.", e);
                return false;
            }

            return true;
        }

        private void ensureModelIndexes() {

            final Mapping modelHeaders = new Mapping(ModelIndexType.MODEL)
                .withFields(
                        new StringMappingField(ModelHeaderField.SUBJECT_TYPE.getName()).withDocValue(true),
                        new StringMappingField(ModelHeaderField.SUBJECT_NAME.getName()).withDocValue(true),
                        new StringMappingField(ModelHeaderField.ENTRY_TYPE.getName()).withDocValue(true),
                        new StringMappingField(ModelHeaderField.ENTRY_NAME.getName()).withDocValue(true),
                        new StringMappingField(ModelHeaderField.ENTRY_DISPLAY_NAME.getName())
                            .withAnalyzed(true)
                            .withCaseInsensitive(true),
                        new StringMappingField(ModelHeaderField.ENTRY_DESCRIPTION.getName())
                            .withAnalyzed(true)
                            .withCaseInsensitive(true),
                        new CompositeMappingField(ModelHeaderField.ENTRY_DETAILS.getName())
                            .withNested(true)
                            .withFields(
                                    new StringMappingField(ModelHeaderField.DETAIL_KEY.getName()).withDocValue(true),
                                    new StringMappingField(ModelHeaderField.DETAIL_VALUE.getName()).withDocValue(true)));

            MappingRequestContext mCtx = MappingRequestContext.builder()
                    .entity(ModelIndexType.INDEX_NAME)
                    .storageId(SecurityUtils.getCurrentUserStorageId())
                    .mapping(modelHeaders)
                    .build();

            searchService.process(mCtx);
        }

        private void ensureDataIndexes() {

            // UN-6722
            for (StorageElement sme : metaModelService.getStorageInstance().getActive()) {

                DataModelInstance i = metaModelService.instance(Descriptors.DATA, sme.getStorageId(), ModelUtils.DEFAULT_MODEL_INSTANCE_ID);

                updateEntityMappings(sme.getStorageId(), false,
                    Stream.concat(i.getLookups().stream(), i.getRegisters().stream())
                        .map(EntityElement::getName)
                        .collect(Collectors.toList()));

                updateRelationMappings(sme.getStorageId(), null, i.getRelations().stream()
                        .map(EntityElement::getName)
                        .collect(Collectors.toList()));
            }
        }
    };

    @Autowired
    private MetaModelService metaModelService;

    @Autowired
    private SearchService searchService;

    @Autowired
    private ConfigurationActionService configurationActionService;
    /**
     * Constructor.
     */
    public DataModelMappingComponent() {
        super();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void afterModuleStartup() {
        EtalonIndexType.ETALON.addChild(EntityIndexType.RECORD);
        EtalonIndexType.ETALON.addChild(EntityIndexType.RELATION);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void afterPlatformStartup() {
        configurationActionService.execute(ensureIndexesAction);
    }

    /**
     * {@inheritDoc}
     */
    public void cleanMetaModelIndex() {
        searchService.deleteFoundResult(
            SearchRequestContext.builder(ModelIndexType.MODEL, ModelIndexType.INDEX_NAME)
                .skipEtalonId(true)
                .fetchAll(true)
                .storageId(SecurityUtils.getCurrentUserStorageId())
                .build());
    }

    /**
     * {@inheritDoc}
     */
    public void removeFromMetaModelIndex(String... entityNames) {

        if (ArrayUtils.isEmpty(entityNames)) {
            return;
        }

        String storageId = SecurityUtils.getCurrentUserStorageId();
        List<FormField> fields = Stream.of(entityNames)
            .map(name -> FormField.exact(ModelHeaderField.SUBJECT_NAME, name))
            .collect(Collectors.toList());

        if (fields.isEmpty()) {
            return;
        }

        searchService.deleteFoundResult(SearchRequestContext.builder(ModelIndexType.MODEL, ModelIndexType.INDEX_NAME)
                .skipEtalonId(true)
                .query(SearchQuery.formQuery(FieldsGroup.or(fields)))
                .storageId(storageId)
                .build());
    }
    /**
     * {@inheritDoc}
     */
    public void putToMetaModelIndex(Collection<ModelSearchEntry> objects) {

        if (CollectionUtils.isEmpty(objects)) {
            return;
        }

        List<Indexing> indexings = objects.stream()
                .filter(Objects::nonNull)
                .map(element ->
                    new Indexing(ModelIndexType.MODEL, null)
                        .withFields(
                                IndexingField.of(ModelHeaderField.SUBJECT_TYPE.getName(), element.getSubjectType()),
                                IndexingField.of(ModelHeaderField.SUBJECT_NAME.getName(), element.getSubjectName()),
                                IndexingField.of(ModelHeaderField.ENTRY_TYPE.getName(), element.getEntryType()),
                                IndexingField.of(ModelHeaderField.ENTRY_NAME.getName(), element.getEntryName()),
                                IndexingField.of(ModelHeaderField.ENTRY_DESCRIPTION.getName(), element.getEntryDescription()),
                                IndexingField.of(ModelHeaderField.ENTRY_DISPLAY_NAME.getName(), element.getEntryDisplayName()),
                                IndexingField.ofRecords(ModelHeaderField.ENTRY_DETAILS.getName(), element.getDetails().entrySet().stream()
                                        .map(entry ->
                                            List.of(
                                                IndexingField.of(ModelHeaderField.DETAIL_KEY.getName(), entry.getKey()),
                                                IndexingField.ofStrings(ModelHeaderField.DETAIL_VALUE.getName(), Objects.nonNull(entry.getValue()) ? entry.getValue() : Collections.emptyList())))
                                        .map(IndexingRecordImpl::new)
                                        .collect(Collectors.toList()))))
                .collect(Collectors.toList());

        IndexRequestContext irc = IndexRequestContext.builder()
                .entity(ModelIndexType.INDEX_NAME)
                .storageId(SecurityUtils.getCurrentUserStorageId())
                .refresh(true)
                .index(indexings)
                .build();

        searchService.process(irc);
    }

    /**
     * {@inheritDoc}
     */
    public void dropAllEntityIndexes(String storageId) {

        String selectedStorageId = Objects.isNull(storageId) ? SecurityUtils.getCurrentUserStorageId() : storageId;
        Stream<String> entities = metaModelService.instance(Descriptors.DATA).getRegisters().stream().map(EntityElement::getName);
        Stream<String> lookupEntities = metaModelService.instance(Descriptors.DATA).getLookups().stream().map(EntityElement::getName);
        Stream.concat(entities, lookupEntities).forEach(ent ->
            searchService.dropIndex(
                MappingRequestContext.builder()
                    .storageId(selectedStorageId)
                    .entity(ent)
                    .drop(true)
                    .build()));
    }

    /**
     * {@inheritDoc}
     */
    public void updateEntityMappings(String storageId, boolean force, String... names) {
        if (ArrayUtils.isNotEmpty(names)) {
            updateEntityMappings(storageId, force, Arrays.asList(names));
        }
    }
    /**
     * {@inheritDoc}
     */
    public void updateEntityMappings(String storageId, boolean force, List<String> names) {

        if (CollectionUtils.isEmpty(names)) {
            return;
        }

        String selectedStorageId = Objects.isNull(storageId)
                ? SecurityUtils.getCurrentUserStorageId()
                : storageId;

        DataModelInstance i = metaModelService.instance(Descriptors.DATA);
        for (String entityName : names) {

            EntityElement element = i.getElement(entityName);
            if (Objects.isNull(element) || (!element.isRegister() && !element.isLookup())) {
                LOGGER.warn("Meta oject with name [{}] not found. Skipping.", entityName);
                continue;
            }

            MappingRequestContext ctx;
            if (element.isRegister()) {
                LOGGER.trace("Processing mapping for register [{}].", entityName);
                ctx = processEntityMappings(selectedStorageId, force, element.getRegister(), i);
            } else {
                LOGGER.trace("Processing mapping for lookup [{}].", entityName);
                ctx = processLookupMappings(selectedStorageId, force, element.getLookup());
            }

            searchService.process(ctx);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void updateRelationMappings(String storageId, String entityName, String... relationNames) {
        if (ArrayUtils.isNotEmpty(relationNames)) {
            updateRelationMappings(storageId, entityName, Arrays.asList(relationNames));
        }
    }
    /**
     * {@inheritDoc}
     */
    public void updateRelationMappings(String storageId, String entityName, List<String> relationNames) {

        if (CollectionUtils.isEmpty(relationNames)) {
            return;
        }

        String selectedStorageId = Objects.isNull(storageId)
                ? SecurityUtils.getCurrentUserStorageId()
                : storageId;

        DataModelInstance i = metaModelService.instance(Descriptors.DATA);
        for (String relationName : relationNames) {

            EntityElement element = i.getElement(relationName);
            if (Objects.isNull(element) || !element.isRelation()) {
                LOGGER.warn("Relation object with name [{}] not found. Skipping.", relationName);
                continue;
            }

            RelationElement re = element.getRelation();
            if (Objects.isNull(entityName) || re.getLeft().getName().equals(entityName)) {
                searchService.process(processRelationMappings(selectedStorageId, re.getLeft().getName(), re, i));
            }

            if (Objects.isNull(entityName) || re.getRight().getName().equals(entityName)) {
                searchService.process(processRelationMappings(selectedStorageId, re.getRight().getName(), re, i));
            }
        }
    }

    private MappingRequestContext processEntityMappings(String storageId, boolean force, EntityElement entity, DataModelInstance i) {

        String shards = entity.propertyExists(MetaCustomPropertiesConstants.SEARCH_SHARDS_NUMBER)
                ? entity.getCustomProperty(MetaCustomPropertiesConstants.SEARCH_SHARDS_NUMBER).getValue()
                : null;

        String replicas = entity.propertyExists(MetaCustomPropertiesConstants.SEARCH_REPLICAS_NUMBER)
                ? entity.getCustomProperty(MetaCustomPropertiesConstants.SEARCH_REPLICAS_NUMBER).getValue()
                : null;

        // after upgrade elasticsearch to 6.4 refactoring this place.
        String tokenize = entity.propertyExists(MetaCustomPropertiesConstants.CUSTOM_TOKENIZE_PROPERTY)
                ? entity.getCustomProperty(MetaCustomPropertiesConstants.CUSTOM_TOKENIZE_PROPERTY).getValue()
                : null;

        // custom score strategy
        String scoreStrategy = entity.propertyExists(MetaCustomPropertiesConstants.SEARCH_SIMILARITY_TYPE)
                ? entity.getCustomProperty(MetaCustomPropertiesConstants.SEARCH_SIMILARITY_TYPE).getValue()
                : null;

        Map<String, Object> scoreStrategyParams = entity.getCustomProperties().stream()
            .filter(cp -> cp.getName().startsWith(MetaCustomPropertiesConstants.SEARCH_SIMILARITY_PARAM))
            .collect(Collectors.toMap(cp ->
                StringUtils.substringAfter(cp.getName(), MetaCustomPropertiesConstants.SEARCH_SIMILARITY_PARAM),
                CustomPropertyElement::getValue));

        MappingRequestContext ctx = MappingRequestContext.builder()
                .storageId(storageId)
                .entity(entity.getName())
                .shards(StringUtils.isNotBlank(shards) ? Integer.parseInt(shards) : 0)
                .replicas(StringUtils.isNotBlank(replicas) ? Integer.parseInt(replicas) : 0)
                .whitespace(StringUtils.equals(tokenize, "whitespace"))
                .forceCreate(force)
                .mappings(
                        new Mapping(EtalonIndexType.ETALON),
                        new Mapping(EntityIndexType.RECORD).withFields(() -> processEntityMappingFields(entity, i)))
                .build();

        ctx.scoreStrategy(scoreStrategy);
        ctx.scoreStrategyParams(scoreStrategyParams);

        return ctx;
    }

    private MappingRequestContext processLookupMappings(String storageId, boolean force, EntityElement entity) {

        String shards = entity.propertyExists(MetaCustomPropertiesConstants.SEARCH_SHARDS_NUMBER)
                ? entity.getCustomProperty(MetaCustomPropertiesConstants.SEARCH_SHARDS_NUMBER).getValue()
                : null;

        String replicas = entity.propertyExists(MetaCustomPropertiesConstants.SEARCH_REPLICAS_NUMBER)
                ? entity.getCustomProperty(MetaCustomPropertiesConstants.SEARCH_REPLICAS_NUMBER).getValue()
                : null;

        // after upgrade elasticsearch to 6.4 refactoring this place.
        String tokenize = entity.propertyExists(MetaCustomPropertiesConstants.CUSTOM_TOKENIZE_PROPERTY)
                ? entity.getCustomProperty(MetaCustomPropertiesConstants.CUSTOM_TOKENIZE_PROPERTY).getValue()
                : null;

        // custom score strategy
        String scoreStrategy = entity.propertyExists(MetaCustomPropertiesConstants.SEARCH_SIMILARITY_TYPE)
                ? entity.getCustomProperty(MetaCustomPropertiesConstants.SEARCH_SIMILARITY_TYPE).getValue()
                : null;

        Map<String, Object> scoreStrategyParams = entity.getCustomProperties().stream()
            .filter(cp -> cp.getName().startsWith(MetaCustomPropertiesConstants.SEARCH_SIMILARITY_PARAM))
            .collect(Collectors.toMap(cp ->
                StringUtils.substringAfter(cp.getName(), MetaCustomPropertiesConstants.SEARCH_SIMILARITY_PARAM),
                CustomPropertyElement::getValue));

        MappingRequestContext ctx = MappingRequestContext.builder()
                .storageId(storageId)
                .entity(entity.getName())
                .shards(StringUtils.isNotBlank(shards) ? Integer.parseInt(shards) : 0)
                .replicas(StringUtils.isNotBlank(replicas) ? Integer.parseInt(replicas) : 0)
                .whitespace(StringUtils.equals(tokenize, "whitespace"))
                .forceCreate(force)
                .mappings(
                        new Mapping(EtalonIndexType.ETALON),
                        new Mapping(EntityIndexType.RECORD).withFields(() -> processLookupMappingFields(entity)))
                .build();

        ctx.scoreStrategy(scoreStrategy);
        ctx.scoreStrategyParams(scoreStrategyParams);

        return ctx;
    }

    private MappingRequestContext processRelationMappings(String storageId, String sideEntityName, EntityElement def, DataModelInstance i) {

        String shards = def.propertyExists(MetaCustomPropertiesConstants.SEARCH_SHARDS_NUMBER)
                ? def.getCustomProperty(MetaCustomPropertiesConstants.SEARCH_SHARDS_NUMBER).getValue()
                : null;

        String replicas = def.propertyExists(MetaCustomPropertiesConstants.SEARCH_REPLICAS_NUMBER)
                ? def.getCustomProperty(MetaCustomPropertiesConstants.SEARCH_REPLICAS_NUMBER).getValue()
                : null;

        return MappingRequestContext.builder()
                .storageId(storageId)
                .entity(sideEntityName)
                .shards(StringUtils.isNotBlank(shards) ? Integer.parseInt(shards) : 0)
                .replicas(StringUtils.isNotBlank(replicas) ? Integer.parseInt(replicas) : 0)
                .mappings(new Mapping(EntityIndexType.RELATION).withFields(() -> processRelationMappingFields(def, i)))
                .build();

    }

    private List<MappingField> processEntityMappingFields(EntityElement def, DataModelInstance i) {

        final List<MappingField> recordHeaders = Arrays.asList(
            new StringMappingField(RecordHeaderField.FIELD_ETALON_ID.getName())
                .withDocValue(true),
            new StringMappingField(RecordHeaderField.FIELD_PERIOD_ID.getName())
                .withDocValue(true),
            new StringMappingField(RecordHeaderField.FIELD_ORIGINATOR.getName())
                .withDocValue(true),
            new TimestampMappingField(RecordHeaderField.FIELD_FROM.getName())
                .withFormat(SearchUtils.DEFAULT_TIMESTAMP_TARGET_FORMAT)
                .withDefaultValue(SearchUtils.ES_MIN_FROM),
            new TimestampMappingField(RecordHeaderField.FIELD_TO.getName())
                .withFormat(SearchUtils.DEFAULT_TIMESTAMP_TARGET_FORMAT)
                .withDefaultValue(SearchUtils.ES_MAX_TO),
            new TimestampMappingField(RecordHeaderField.FIELD_CREATED_AT.getName()),
            new TimestampMappingField(RecordHeaderField.FIELD_UPDATED_AT.getName()),
            new BooleanMappingField(RecordHeaderField.FIELD_DELETED.getName())
                .withDefaultValue(Boolean.FALSE),
            new BooleanMappingField(RecordHeaderField.FIELD_INACTIVE.getName())
                .withDefaultValue(Boolean.FALSE),
            new StringMappingField(RecordHeaderField.FIELD_OPERATION_TYPE.getName())
                .withDocValue(true),
            new StringMappingField(RecordHeaderField.FIELD_EXTERNAL_KEYS.getName())
                .withDocValue(true)
        );

        List<MappingField> result = new ArrayList<>(recordHeaders.size() + def.getAttributes().size() + 1);

        result.addAll(recordHeaders);
        result.addAll(processComplexEntityMappingFields(def, i));

        return result;
    }

    private List<MappingField> processLookupMappingFields(EntityElement entity) {

        final List<MappingField> recordHeaders = Arrays.asList(
            new StringMappingField(RecordHeaderField.FIELD_ETALON_ID.getName())
                .withDocValue(true),
            new StringMappingField(RecordHeaderField.FIELD_PERIOD_ID.getName())
                .withDocValue(true),
            new StringMappingField(RecordHeaderField.FIELD_ORIGINATOR.getName())
                .withDocValue(true),
            new TimestampMappingField(RecordHeaderField.FIELD_FROM.getName())
                .withFormat(SearchUtils.DEFAULT_TIMESTAMP_TARGET_FORMAT)
                .withDefaultValue(SearchUtils.ES_MIN_FROM),
            new TimestampMappingField(RecordHeaderField.FIELD_TO.getName())
                .withFormat(SearchUtils.DEFAULT_TIMESTAMP_TARGET_FORMAT)
                .withDefaultValue(SearchUtils.ES_MAX_TO),
            new TimestampMappingField(RecordHeaderField.FIELD_CREATED_AT.getName()),
            new TimestampMappingField(RecordHeaderField.FIELD_UPDATED_AT.getName()),
            new BooleanMappingField(RecordHeaderField.FIELD_DELETED.getName())
                .withDefaultValue(Boolean.FALSE),
            new BooleanMappingField(RecordHeaderField.FIELD_INACTIVE.getName())
                .withDefaultValue(Boolean.FALSE),
            new StringMappingField(RecordHeaderField.FIELD_OPERATION_TYPE.getName())
                .withDocValue(true),
            new StringMappingField(RecordHeaderField.FIELD_EXTERNAL_KEYS.getName())
                .withDocValue(true)
        );

        List<MappingField> result = new ArrayList<>(recordHeaders.size() + entity.getAttributes().size() + 1);
        result.addAll(recordHeaders);

        // Only one level, value type attributes
        // are expected to sit in a lookup
        for (AttributeElement attr : entity.getAttributes().values()) {

            MappingField f = processValueAttribute(attr);
            if (Objects.nonNull(f)) {
                result.add(f);
            }
        }

        return result;
    }

    private List<MappingField> processRelationMappingFields(EntityElement def, DataModelInstance i) {

        final List<MappingField> relationHeaders = Arrays.asList(
            new StringMappingField(RelationHeaderField.FIELD_ETALON_ID.getName())
                .withDocValue(true),
            new StringMappingField(RelationHeaderField.FIELD_FROM_ETALON_ID.getName())
                .withDocValue(true),
            new StringMappingField(RelationHeaderField.FIELD_TO_ETALON_ID.getName())
                .withDocValue(true),
            new StringMappingField(RelationHeaderField.FIELD_PERIOD_ID.getName())
                .withDocValue(true),
            new StringMappingField(RelationHeaderField.FIELD_RELATION_NAME.getName())
                .withDocValue(true),
            new StringMappingField(RelationHeaderField.FIELD_RELATION_TYPE.getName())
                .withDocValue(true),
            new StringMappingField(RelationHeaderField.FIELD_ORIGINATOR.getName())
                .withDocValue(true),
            new StringMappingField(RelationHeaderField.FIELD_OPERATION_TYPE.getName())
                .withDocValue(true),
            new TimestampMappingField(RelationHeaderField.FIELD_FROM.getName())
                .withFormat(SearchUtils.DEFAULT_TIMESTAMP_TARGET_FORMAT)
                .withDefaultValue(SearchUtils.ES_MIN_FROM),
            new TimestampMappingField(RelationHeaderField.FIELD_TO.getName())
                .withFormat(SearchUtils.DEFAULT_TIMESTAMP_TARGET_FORMAT)
                .withDefaultValue(SearchUtils.ES_MAX_TO),
            new TimestampMappingField(RelationHeaderField.FIELD_CREATED_AT.getName()),
            new TimestampMappingField(RelationHeaderField.FIELD_UPDATED_AT.getName()),
            new BooleanMappingField(RelationHeaderField.FIELD_DELETED.getName())
                .withDefaultValue(Boolean.FALSE),
            new BooleanMappingField(RelationHeaderField.FIELD_INACTIVE.getName())
                .withDefaultValue(Boolean.FALSE),
            new BooleanMappingField(RelationHeaderField.FIELD_DIRECTION_FROM.getName())
                .withDefaultValue(Boolean.FALSE)
        );

        List<MappingField> result = new ArrayList<>(relationHeaders.size() + def.getAttributes().size() + 1);
        result.addAll(relationHeaders);
        result.addAll(processComplexEntityMappingFields(def, i));

        return result;
    }

    private List<MappingField> processComplexEntityMappingFields(EntityElement def, DataModelInstance i) {

        List<MappingField> result = new ArrayList<>(def.getAttributes().size());
        for (AttributeElement attr : def.getAttributes().values()) {

            // 0. Only first level attributes processed
            if (attr.getLevel() > 0) {
                continue;
            }

            MappingField f = null;
            if (attr.isSimple() || attr.isArray() || attr.isCode()) {
                f = processValueAttribute(attr);
            } else if (attr.isComplex()) {

                EntityElement nested = i.getNested(attr.getComplex().getNestedEntityName());
                if (nested == null) {

                    String message = "Invalid model. Nested entity [{}] from complex attribute [{}] not found.";
                    LOGGER.warn(message, attr.getComplex().getNestedEntityName(), attr.getName());
                    throw new PlatformFailureException(message,
                            MetaExceptionIds.EX_META_MAPPING_NESTED_ENTITY_NOT_FOUND,
                            attr.getComplex().getNestedEntityName(), attr.getName());
                }

                Collection<MappingField> fields = processComplexEntityMappingFields(nested, i);
                if (CollectionUtils.isNotEmpty(fields)) {
                    f = new CompositeMappingField(attr.getName())
                            .withFields(fields);
                }
            }

            if (Objects.nonNull(f)) {
                result.add(f);
            }
        }

        return result;
    }
    /**
     * Processes a simple attribute from EntityDef.
     * @param attr    the attribute
     * @param builder the builder
     *
     * @return the builder
     * @throws IOException
     */
    private MappingField processValueAttribute(AttributeElement attr) {

        if (attr == null || (!attr.isIndexed() && !attr.hasIndexingParams())) {
            return null;
        }

        String name = attr.getName();
        AttributeValueType valueType = attr.getValueType();

        switch (valueType) {
        case BLOB:
        case CLOB:
        case STRING:
            return new StringMappingField(name)
                    .withMorphologicalAnalysis(attr.hasIndexingParams() && attr.getIndexingParams().isMorphological())
                    .withCaseInsensitive(attr.hasIndexingParams() && attr.getIndexingParams().isCaseInsensitive())
                    .withAnalyzed(attr.isIndexed() && attr.getIndexed().isAnalyzed());
        case BOOLEAN:
            return new BooleanMappingField(name);
        case INTEGER:
            return new LongMappingField(name);
        case MEASURED:
        case NUMBER:
            return new DoubleMappingField(name);
        case DATE:
            return new DateMappingField(name);
        case TIME:
            return new TimeMappingField(name);
        case TIMESTAMP:
            return new TimestampMappingField(name);
        default:
            break;
        }

        // Would be better to thow.
        return null;
    }
}
